本範例參考改寫此專案,大部分轉物件並套入自訂框架
先來建立一個註冊畫面吧
補上一個使用 bootstrap 樣式套件的頁首
view/header/default.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><?php if(isset($title)){ echo $title; }?></title>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.0/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
加上這個樣式套件,在表單上加上一些 style class 就會漂亮起來
view/body/register.php
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-8 col-md-6 col-sm-offset-2 col-md-offset-3">
<form role="form" method="post" action="" autocomplete="off">
<h2>Please Sign Up</h2>
<p>Already a member? <a href='login.php'>Login</a></p>
<hr>
<?php
//check for any errors
if(isset($error)){
foreach($error as $error){
echo '<p class="bg-danger">'.$error.'</p>';
}
}
//if action is joined show sucess
if(isset($_GET['action']) && $_GET['action'] == 'joined'){
echo "<h2 class='bg-success'>Registration successful, please check your email to activate your account.</h2>";
}
?>
<div class="form-group">
<input type="text" name="username" id="username" class="form-control input-lg" placeholder="User Name" value="<?php if(isset($error)){ echo htmlspecialchars($_POST['username'], ENT_QUOTES); } ?>" tabindex="1">
</div>
<div class="form-group">
<input type="email" name="email" id="email" class="form-control input-lg" placeholder="Email Address" value="<?php if(isset($error)){ echo htmlspecialchars($_POST['email'], ENT_QUOTES); } ?>" tabindex="2">
</div>
<div class="row">
<div class="col-xs-6 col-sm-6 col-md-6">
<div class="form-group">
<input type="password" name="password" id="password" class="form-control input-lg" placeholder="Password" tabindex="3">
</div>
</div>
<div class="col-xs-6 col-sm-6 col-md-6">
<div class="form-group">
<input type="password" name="passwordConfirm" id="passwordConfirm" class="form-control input-lg" placeholder="Confirm Password" tabindex="4">
</div>
</div>
</div>
<div class="row">
<div class="col-xs-6 col-md-6"><input type="submit" name="submit" value="Register" class="btn btn-primary btn-block btn-lg" tabindex="5"></div>
</div>
</form>
</div>
</div>
</div>
在來就是路由上新增一個 case "register"
要做的事情有幾樣
include('view/header/default.php'); // 載入共用的頁首
include('view/body/register.php'); // 載入註冊用的表單
include('view/footer/default.php'); // 載入共用的頁尾
$gump = new GUMP();
$_POST = $gump->sanitize($_POST);
$validation_rules_array = array(
'username' => 'required|alpha_numeric|max_len,20|min_len,8',
'email' => 'required|valid_email',
'password' => 'required|max_len,20|min_len,8',
'passwordConfirm' => 'required'
);
$gump->validation_rules($validation_rules_array);
$filter_rules_array = array(
'username' => 'trim|sanitize_string',
'email' => 'trim|sanitize_email',
'password' => 'trim',
'passwordConfirm' => 'trim'
);
$gump->filter_rules($filter_rules_array);
$validated_data = $gump->run($_POST);
if($validated_data === false) {
$error = $gump->get_readable_errors(false);
}
新增 validators/UserVeridator.php
<?php
/**
* 耦合使用 Database 物件進行資料庫驗證 username 與 email 是否已存在於資料庫
*/
class UserVeridator {
private $error;
/**
* 可取出錯誤訊息字串陣列
*/
public function getErrorArray(){
return $this->error;
}
/**
* 驗證二次密碼輸入是否相符
*/
public function isPasswordMatch($password, $passwrodConfirm){
if ($password != $passwrodConfirm){
$this->error[] = 'Passwords do not match.';
return false;
}
return true;
}
/**
* 驗證帳號是否已存在於資料庫中
*/
public function isUsernameDuplicate($username){
$result = Database::get()->execute('SELECT username FROM members WHERE username = :username', array(':username' => $username));
if(isset($result[0]['username']) and !empty($result[0]['username'])){
$this->error[] = 'Username provided is already in use.';
return false;
}
return true;
}
/**
* 驗證信箱是否已存在於資料庫中
*/
public function isEmailDuplicate($email){
$result = Database::get()->execute('SELECT email FROM members WHERE email = :email', array(':email' => $email));
if(isset($result[0]['email']) AND !empty($result[0]['email'])){
$this->error[] = 'Email provided is already in use.';
return false;
}
return true;
}
}
記得要把 validators 資料夾名稱 加到 composer 的 autoload class map 裡面
{
"name": "jarvis/game",
"authors": [
{
"name": "Jarvis",
"email": "endless640c@gmail.com"
}
],
"require": {
"monolog/monolog": "^1.23",
"wixel/gump": "^1.5",
"fightbulc/moment": "^1.26",
"phpmailer/phpmailer": "^6.0"
},
"autoload": {
"classmap": [
"validators",
"libraries",
"config"
]
}
}
再執行 composer dump 就可以使用囉
route.php
// validation successful
foreach($validation_rules_array as $key => $val) {
${$key} = $_POST[$key];
}
$userVeridator = new UserVeridator();
$userVeridator->isPasswordMatch($password, $passwordConfirm);
$userVeridator->isUsernameDuplicate($username);
$userVeridator->isEmailDuplicate($email);
$error = $userVeridator->getErrorArray();
新增 libraries/Password.php 來處理密碼加密
<?php
if (!defined('PASSWORD_BCRYPT')) {
define('PASSWORD_BCRYPT', 1);
define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
}
Class Password {
public function __construct() {}
/**
* Hash the password using the specified algorithm
*
* @param string $password The password to hash
* @param int $algo The algorithm to use (Defined by PASSWORD_* constants)
* @param array $options The options for the algorithm to use
*
* @return string|false The hashed password, or false on error.
*/
public function password_hash($password, $algo, array $options = array()) {
if (!function_exists('crypt')) {
trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
return null;
}
if (!is_string($password)) {
trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
return null;
}
if (!is_int($algo)) {
trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
return null;
}
switch ($algo) {
case PASSWORD_BCRYPT :
// Note that this is a C constant, but not exposed to PHP, so we don't define it here.
$cost = 10;
if (isset($options['cost'])) {
$cost = $options['cost'];
if ($cost < 4 || $cost > 31) {
trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
return null;
}
}
// The length of salt to generate
$raw_salt_len = 16;
// The length required in the final serialization
$required_salt_len = 22;
$hash_format = sprintf("$2y$%02d$", $cost);
break;
default :
trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
return null;
}
if (isset($options['salt'])) {
switch (gettype($options['salt'])) {
case 'NULL' :
case 'boolean' :
case 'integer' :
case 'double' :
case 'string' :
$salt = (string)$options['salt'];
break;
case 'object' :
if (method_exists($options['salt'], '__tostring')) {
$salt = (string)$options['salt'];
break;
}
case 'array' :
case 'resource' :
default :
trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
return null;
}
if (strlen($salt) < $required_salt_len) {
trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING);
return null;
} elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
$salt = str_replace('+', '.', base64_encode($salt));
}
} else {
$salt = str_replace('+', '.', base64_encode($this->generate_entropy($required_salt_len)));
}
$salt = substr($salt, 0, $required_salt_len);
$hash = $hash_format . $salt;
$ret = crypt($password, $hash);
if (!is_string($ret) || strlen($ret) <= 13) {
return false;
}
return $ret;
}
/**
* Generates Entropy using the safest available method, falling back to less preferred methods depending on support
*
* @param int $bytes
*
* @return string Returns raw bytes
*/
function generate_entropy($bytes){
$buffer = '';
$buffer_valid = false;
if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
$buffer = mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM);
if ($buffer) {
$buffer_valid = true;
}
}
if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
$buffer = openssl_random_pseudo_bytes($bytes);
if ($buffer) {
$buffer_valid = true;
}
}
if (!$buffer_valid && is_readable('/dev/urandom')) {
$f = fopen('/dev/urandom', 'r');
$read = strlen($buffer);
while ($read < $bytes) {
$buffer .= fread($f, $bytes - $read);
$read = strlen($buffer);
}
fclose($f);
if ($read >= $bytes) {
$buffer_valid = true;
}
}
if (!$buffer_valid || strlen($buffer) < $bytes) {
$bl = strlen($buffer);
for ($i = 0; $i < $bytes; $i++) {
if ($i < $bl) {
$buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
} else {
$buffer .= chr(mt_rand(0, 255));
}
}
}
return $buffer;
}
/**
* Get information about the password hash. Returns an array of the information
* that was used to generate the password hash.
*
* array(
* 'algo' => 1,
* 'algoName' => 'bcrypt',
* 'options' => array(
* 'cost' => 10,
* ),
* )
*
* @param string $hash The password hash to extract info from
*
* @return array The array of information about the hash.
*/
function password_get_info($hash) {
$return = array('algo' => 0, 'algoName' => 'unknown', 'options' => array(), );
if (substr($hash, 0, 4) == '$2y$' && strlen($hash) == 60) {
$return['algo'] = PASSWORD_BCRYPT;
$return['algoName'] = 'bcrypt';
list($cost) = sscanf($hash, "$2y$%d$");
$return['options']['cost'] = $cost;
}
return $return;
}
/**
* Determine if the password hash needs to be rehashed according to the options provided
*
* If the answer is true, after validating the password using password_verify, rehash it.
*
* @param string $hash The hash to test
* @param int $algo The algorithm used for new password hashes
* @param array $options The options array passed to password_hash
*
* @return boolean True if the password needs to be rehashed.
*/
function password_needs_rehash($hash, $algo, array $options = array()) {
$info = password_get_info($hash);
if ($info['algo'] != $algo) {
return true;
}
switch ($algo) {
case PASSWORD_BCRYPT :
$cost = isset($options['cost']) ? $options['cost'] : 10;
if ($cost != $info['options']['cost']) {
return true;
}
break;
}
return false;
}
/**
* Verify a password against a hash using a timing attack resistant approach
*
* @param string $password The password to verify
* @param string $hash The hash to verify against
*
* @return boolean If the password matches the hash
*/
public function password_verify($password, $hash) {
if (!function_exists('crypt')) {
trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
return false;
}
$ret = crypt($password, $hash);
if (!is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) {
return false;
}
$status = 0;
for ($i = 0; $i < strlen($ret); $i++) {
$status |= (ord($ret[$i]) ^ ord($hash[$i]));
}
return $status === 0;
}
}
route.php 整個組合起來:
switch($route->getParameter(1)){
case "register";
if(isset($_POST['submit']))
{
$gump = new GUMP();
$_POST = $gump->sanitize($_POST);
$validation_rules_array = array(
'username' => 'required|alpha_numeric|max_len,20|min_len,8',
'email' => 'required|valid_email',
'password' => 'required|max_len,20|min_len,8',
'passwordConfirm' => 'required'
);
$gump->validation_rules($validation_rules_array);
$filter_rules_array = array(
'username' => 'trim|sanitize_string',
'email' => 'trim|sanitize_email',
'password' => 'trim',
'passwordConfirm' => 'trim'
);
$gump->filter_rules($filter_rules_array);
$validated_data = $gump->run($_POST);
if($validated_data === false) {
$error = $gump->get_readable_errors(false);
} else {
// validation successful
foreach($validation_rules_array as $key => $val) {
${$key} = $_POST[$key];
}
$userVeridator = new UserVeridator();
$userVeridator->isPasswordMatch($password, $passwordConfirm);
$userVeridator->isUsernameDuplicate($username);
$userVeridator->isEmailDuplicate($email);
$error = $userVeridator->getErrorArray();
}
//if no errors have been created carry on
if(count($error) == 0)
{
//hash the password
$passwordObject = new Password();
$hashedpassword = $passwordObject->password_hash($password, PASSWORD_BCRYPT);
//create the random activasion code
$activasion = md5(uniqid(rand(),true));
try {
// 新增到資料庫
$data_array = array(
'username' => $username,
'password' => $hashedpassword,
'email' => $email,
'active' => $activasion
);
Database::get()->insert("members", $data_array);
//redirect to index page
header('Location: '.Config::BASE_URL.'register');
//else catch the exception and show the error.
} catch(PDOException $e) {
$error[] = $e->getMessage();
}
}
}
include('view/header/default.php'); // 載入共用的頁首
include('view/body/register.php'); // 載入註冊用的表單
include('view/footer/default.php'); // 載入共用的頁尾
break;
//...以下重複略...
}
最終就達成我們要的樣子,列出錯誤的資訊
PASSWORD_BCRYPT 預設鹽值已更新為 '2y'
password_hash 執行時判定 $algo 為數字會報錯,正確應為字串
if (!is_int($algo)) { // 會報錯
if (!is_string($algo)) { // 要改成這樣